/*
 * Copyright 2020 The gRPC Authors
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package io.grpc.binder;

import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Preconditions.checkState;

import android.content.Context;
import com.google.errorprone.annotations.DoNotCall;
import io.grpc.ExperimentalApi;
import io.grpc.ForwardingChannelBuilder;
import io.grpc.ManagedChannel;
import io.grpc.ManagedChannelBuilder;
import io.grpc.binder.internal.BinderClientTransportFactory;
import io.grpc.internal.FixedObjectPool;
import io.grpc.internal.ManagedChannelImplBuilder;
import java.util.concurrent.Executor;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import javax.annotation.Nullable;

/**
 * Builder for a gRPC channel which communicates with an Android bound service.
 *
 * @see <a href="https://developer.android.com/guide/components/bound-services.html">Bound
 *     Services</a>
 */
@ExperimentalApi("https://github.com/grpc/grpc-java/issues/8022")
public final class BinderChannelBuilder extends ForwardingChannelBuilder<BinderChannelBuilder> {

  /**
   * Creates a channel builder that will bind to a remote Android service.
   *
   * <p>The underlying Android binding will be torn down when the channel becomes idle. This happens
   * after 30 minutes without use by default but can be configured via {@link
   * ManagedChannelBuilder#idleTimeout(long, TimeUnit)} or triggered manually with {@link
   * ManagedChannel#enterIdle()}.
   *
   * <p>You the caller are responsible for managing the lifecycle of any channels built by the
   * resulting builder. They will not be shut down automatically.
   *
   * @param directAddress the {@link AndroidComponentAddress} referencing the service to bind to.
   * @param sourceContext the context to bind from (e.g. The current Activity or Application).
   * @return a new builder
   */
  public static BinderChannelBuilder forAddress(
      AndroidComponentAddress directAddress, Context sourceContext) {
    return new BinderChannelBuilder(
        checkNotNull(directAddress, "directAddress"),
        null,
        sourceContext,
        BinderChannelCredentials.forDefault());
  }

  /**
   * Creates a channel builder that will bind to a remote Android service with provided
   * BinderChannelCredentials.
   *
   * <p>The underlying Android binding will be torn down when the channel becomes idle. This happens
   * after 30 minutes without use by default but can be configured via {@link
   * ManagedChannelBuilder#idleTimeout(long, TimeUnit)} or triggered manually with {@link
   * ManagedChannel#enterIdle()}.
   *
   * <p>You the caller are responsible for managing the lifecycle of any channels built by the
   * resulting builder. They will not be shut down automatically.
   *
   * @param directAddress the {@link AndroidComponentAddress} referencing the service to bind to.
   * @param sourceContext the context to bind from (e.g. The current Activity or Application).
   * @param channelCredentials the arbitrary binder specific channel credentials to be used to
   *     establish a binder connection.
   * @return a new builder
   */
  @ExperimentalApi("https://github.com/grpc/grpc-java/issues/10173")
  public static BinderChannelBuilder forAddress(
      AndroidComponentAddress directAddress,
      Context sourceContext,
      BinderChannelCredentials channelCredentials) {
    return new BinderChannelBuilder(
        checkNotNull(directAddress, "directAddress"), null, sourceContext, channelCredentials);
  }

  /**
   * Creates a channel builder that will bind to a remote Android service, via a string target name
   * which will be resolved.
   *
   * <p>The underlying Android binding will be torn down when the channel becomes idle. This happens
   * after 30 minutes without use by default but can be configured via {@link
   * ManagedChannelBuilder#idleTimeout(long, TimeUnit)} or triggered manually with {@link
   * ManagedChannel#enterIdle()}.
   *
   * <p>You the caller are responsible for managing the lifecycle of any channels built by the
   * resulting builder. They will not be shut down automatically.
   *
   * @param target A target uri which should resolve into an {@link AndroidComponentAddress}
   *     referencing the service to bind to.
   * @param sourceContext the context to bind from (e.g. The current Activity or Application).
   * @return a new builder
   */
  public static BinderChannelBuilder forTarget(String target, Context sourceContext) {
    return new BinderChannelBuilder(
        null, checkNotNull(target, "target"), sourceContext, BinderChannelCredentials.forDefault());
  }

  /**
   * Creates a channel builder that will bind to a remote Android service, via a string target name
   * which will be resolved.
   *
   * <p>The underlying Android binding will be torn down when the channel becomes idle. This happens
   * after 30 minutes without use by default but can be configured via {@link
   * ManagedChannelBuilder#idleTimeout(long, TimeUnit)} or triggered manually with {@link
   * ManagedChannel#enterIdle()}.
   *
   * <p>You the caller are responsible for managing the lifecycle of any channels built by the
   * resulting builder. They will not be shut down automatically.
   *
   * @param target A target uri which should resolve into an {@link AndroidComponentAddress}
   *     referencing the service to bind to.
   * @param sourceContext the context to bind from (e.g. The current Activity or Application).
   * @param channelCredentials the arbitrary binder specific channel credentials to be used to
   *     establish a binder connection.
   * @return a new builder
   */
  @ExperimentalApi("https://github.com/grpc/grpc-java/issues/10173")
  public static BinderChannelBuilder forTarget(
      String target, Context sourceContext, BinderChannelCredentials channelCredentials) {
    return new BinderChannelBuilder(
        null, checkNotNull(target, "target"), sourceContext, channelCredentials);
  }

  /** Always fails. Call {@link #forAddress(AndroidComponentAddress, Context)} instead. */
  @DoNotCall("Unsupported. Use forAddress(AndroidComponentAddress, Context) instead")
  public static BinderChannelBuilder forAddress(String name, int port) {
    throw new UnsupportedOperationException(
        "call forAddress(AndroidComponentAddress, Context) instead");
  }

  /** Always fails. Call {@link #forAddress(AndroidComponentAddress, Context)} instead. */
  @DoNotCall("Unsupported. Use forTarget(String, Context) instead")
  public static BinderChannelBuilder forTarget(String target) {
    throw new UnsupportedOperationException(
        "call forAddress(AndroidComponentAddress, Context) instead");
  }

  private final ManagedChannelImplBuilder managedChannelImplBuilder;
  private final BinderClientTransportFactory.Builder transportFactoryBuilder;

  private boolean strictLifecycleManagement;

  private BinderChannelBuilder(
      @Nullable AndroidComponentAddress directAddress,
      @Nullable String target,
      Context sourceContext,
      BinderChannelCredentials channelCredentials) {
    transportFactoryBuilder =
        new BinderClientTransportFactory.Builder()
            .setSourceContext(sourceContext)
            .setChannelCredentials(channelCredentials);

    if (directAddress != null) {
      managedChannelImplBuilder =
          new ManagedChannelImplBuilder(
              directAddress, directAddress.getAuthority(), transportFactoryBuilder, null);
    } else {
      managedChannelImplBuilder =
          new ManagedChannelImplBuilder(target, transportFactoryBuilder, null);
    }
    idleTimeout(60, TimeUnit.SECONDS);
  }

  @Override
  @SuppressWarnings("deprecation") // Not extending ForwardingChannelBuilder2 to preserve ABI.
  protected ManagedChannelBuilder<?> delegate() {
    return managedChannelImplBuilder;
  }

  /** Specifies certain optional aspects of the underlying Android Service binding. */
  public BinderChannelBuilder setBindServiceFlags(BindServiceFlags bindServiceFlags) {
    transportFactoryBuilder.setBindServiceFlags(bindServiceFlags);
    return this;
  }

  /**
   * Provides a custom scheduled executor service.
   *
   * <p>This is an optional parameter. If the user has not provided a scheduled executor service
   * when the channel is built, the builder will use a static cached thread pool.
   *
   * @return this
   */
  public BinderChannelBuilder scheduledExecutorService(
      ScheduledExecutorService scheduledExecutorService) {
    transportFactoryBuilder.setScheduledExecutorPool(
        new FixedObjectPool<>(checkNotNull(scheduledExecutorService, "scheduledExecutorService")));
    return this;
  }

  /**
   * Provides a custom {@link Executor} for accessing this application's main thread.
   *
   * <p>Optional. A default implementation will be used if no custom Executor is provided.
   *
   * @return this
   */
  public BinderChannelBuilder mainThreadExecutor(Executor mainThreadExecutor) {
    transportFactoryBuilder.setMainThreadExecutor(mainThreadExecutor);
    return this;
  }

  /**
   * Provides a custom security policy.
   *
   * <p>This is optional. If the user has not provided a security policy, this channel will only
   * communicate with the same application UID.
   *
   * @return this
   */
  public BinderChannelBuilder securityPolicy(SecurityPolicy securityPolicy) {
    transportFactoryBuilder.setSecurityPolicy(securityPolicy);
    return this;
  }

  /** Sets the policy for inbound parcelable objects. */
  public BinderChannelBuilder inboundParcelablePolicy(
      InboundParcelablePolicy inboundParcelablePolicy) {
    transportFactoryBuilder.setInboundParcelablePolicy(inboundParcelablePolicy);
    return this;
  }

  /**
   * Disables the channel idle timeout and prevents it from being enabled. This allows a centralized
   * application method to configure the channel builder and return it, without worrying about
   * another part of the application accidentally enabling the idle timeout.
   */
  public BinderChannelBuilder strictLifecycleManagement() {
    strictLifecycleManagement = true;
    super.idleTimeout(1000, TimeUnit.DAYS); // >30 days disables timeouts entirely.
    return this;
  }

  /**
   * Checks servers against this Channel's {@link SecurityPolicy} *before* binding.
   *
   * <p>Android users can be tricked into installing a malicious app with the same package name as a
   * legitimate server. That's why we don't send calls to a server until it has been authorized by
   * an appropriate {@link SecurityPolicy}. But merely binding to a malicious server can enable
   * "keep-alive" and "background activity launch" abuse, even if it's ultimately unauthorized.
   * Pre-authorization mitigates these threats by performing a preliminary {@link SecurityPolicy}
   * check against a server app's PackageManager-registered identity without actually creating an
   * instance of it. This is especially important for security when the server's direct address
   * isn't known in advance but rather resolved via target URI or discovered by other means.
   *
   * <p>Note that, unlike ordinary authorization, pre-authorization is performed against the server
   * app's UID, not the UID of the process hosting the bound Service. These can be different, most
   * commonly due to services that set `android:isolatedProcess=true`.
   *
   * <p>Pre-authorization is strongly recommended but it remains optional for now because of this
   * behavior change and the small performance cost.
   *
   * <p>The default value of this property is false but it will become true in a future release.
   * Clients that require a particular behavior should configure it explicitly using this method
   * rather than relying on the default.
   */
  @ExperimentalApi("https://github.com/grpc/grpc-java/issues/12191")
  public BinderChannelBuilder preAuthorizeServers(boolean preAuthorize) {
    transportFactoryBuilder.setPreAuthorizeServers(preAuthorize);
    return this;
  }

  /**
   * Specifies how and when to authorize a server against this Channel's {@link SecurityPolicy}.
   *
   * <p>This method selects the original "legacy" authorization strategy, which is no longer
   * preferred for two reasons: First, the legacy strategy considers the UID of the server *process*
   * we connect to. This is problematic for services using the `android:isolatedProcess` attribute,
   * which runs them under a different "ephemeral" UID. This UID lacks all the privileges of the
   * hosting app -- any non-trivial SecurityPolicy would fail to authorize it. Second, the legacy
   * authorization strategy performs SecurityPolicy checks later in the connection handshake, which
   * means the calling UID must be rechecked on every subsequent RPC. For these reasons, prefer
   * {@link #useV2AuthStrategy} instead.
   *
   * <p>The server does not know which authorization strategy a client is using. Both strategies
   * work with all versions of the grpc-binder server.
   *
   * <p>Callers need not specify an authorization strategy, but the default is unspecified and will
   * eventually become {@link #useV2AuthStrategy()}. Clients that require the legacy strategy should
   * configure it explicitly using this method. Eventually, however, legacy support will be
   * deprecated and removed.
   *
   * @return this
   */
  @ExperimentalApi("https://github.com/grpc/grpc-java/issues/12397")
  public BinderChannelBuilder useLegacyAuthStrategy() {
    transportFactoryBuilder.setUseLegacyAuthStrategy(true);
    return this;
  }

  /**
   * Specifies how and when to authorize a server against this Channel's {@link SecurityPolicy}.
   *
   * <p>This method selects the v2 authorization strategy. It improves on the original strategy
   * ({@link #useLegacyAuthStrategy}), by considering the UID of the server *app* we connect to,
   * rather than the server *process*. This allows clients to connect to services configured with
   * the `android:isolatedProcess` attribute, which run with the same authority as the hosting app,
   * but under a different "ephemeral" UID that any non-trivial SecurityPolicy would fail to
   * authorize.
   *
   * <p>Furthermore, the v2 authorization strategy performs SecurityPolicy checks earlier in the
   * connection handshake, which allows subsequent RPCs over that connection to proceed securely
   * without further UID checks. For these reasons, clients should prefer the v2 strategy.
   *
   * <p>The server does not know which authorization strategy a client is using. Both strategies
   * work with all versions of the grpc-binder server.
   *
   * <p>Callers need not specify an authorization strategy, but the default is unspecified and can
   * change over time. Clients that require the v2 strategy should configure it explicitly using
   * this method. Eventually, this strategy will become the default and legacy support will be
   * removed.
   *
   * <p>If moving to the new authorization strategy causes a robolectric test to fail, ensure your
   * fake Service component is registered with `ShadowPackageManager` using `addOrUpdateService()`.
   *
   * @return this
   */
  @ExperimentalApi("https://github.com/grpc/grpc-java/issues/12397")
  public BinderChannelBuilder useV2AuthStrategy() {
    transportFactoryBuilder.setUseLegacyAuthStrategy(false);
    return this;
  }

  @Override
  public BinderChannelBuilder idleTimeout(long value, TimeUnit unit) {
    checkState(
        !strictLifecycleManagement,
        "Idle timeouts are not supported when strict lifecycle management is enabled");
    super.idleTimeout(value, unit);
    return this;
  }

  @Override
  public ManagedChannel build() {
    transportFactoryBuilder.setOffloadExecutorPool(
        managedChannelImplBuilder.getOffloadExecutorPool());
    setNameResolverArg(
        ApiConstants.SOURCE_ANDROID_CONTEXT, transportFactoryBuilder.getSourceContext());
    return super.build();
  }
}
